﻿//++
//
// Copyright (c) Microsoft Corporation.  All rights reserved.
//
// Module Name:
//
//  ExchangeHelpers.cs
//    
// Abstract:
//
//  This module implements the ExchangeHelpers class.
//
//--

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using Microsoft.OfficeCommunicationsServer.Applications.Common;

namespace Microsoft.EnhancedReminders.Scheduler
{
    internal static class ExchangeHelpers
    {
        // Message class for voicemails
        private const string _voicemailItemClass = "IPM.Note.Microsoft.Voicemail.UM.CA";
        
        // Message class for calendar items
        private const string _appointmentItemClass = "IPM.Appointment";

        // Start and end tags for the conference address encoded in the JoinInfo Live Meeting property
        private const string _confUriStart = "<ConferenceURI Value=\"";
        private const string _confUriEnd = "\"/>";

        // Property tag values for the extended properties that are used with voicemail
        internal const string CallerIdPropertyTag = "0x6802";
        internal const string FromNamePropertyTag = "0x42";

        /// <summary>
        /// Gets the MailboxType corresponding to the specified mailbox item.
        /// </summary>
        /// <param name="item">Mailbox item</param>
        /// <remarks>
        /// The MailboxType is determined from the item class.
        /// </remarks>
        /// <returns>
        /// The MailboxType corresponding to the specified mailbox item.
        /// </returns>
        internal static MailboxType GetMailboxTypeFromMessage(ItemType item)
        {
            Debug.Assert(item != null, "item != null");

            MailboxType mailboxType = MailboxType.Unknown;

            if (item.ItemClass == _voicemailItemClass)
            {
                mailboxType = MailboxType.Voicemail;
            }
            else if (item.ItemClass.StartsWith(_appointmentItemClass, StringComparison.OrdinalIgnoreCase))
            {
                mailboxType = MailboxType.CalendarItem;
            }

            return mailboxType;
        }

        /// <summary>
        /// Indicates if the specified calendar item is a Live Meeting request.
        /// </summary>
        /// <param name="calendarItem">Calendar item</param>
        /// <returns>
        /// True if the calendar item is a Live Meeting request; false otherwise.
        /// </returns>
        internal static bool IsLiveMeeting(CalendarItemType calendarItem)
        {
            Debug.Assert(calendarItem != null, "calendarItem != null");
            return calendarItem.ItemClass == Constants.LiveMeetingMessageClass;
        }

        /// <summary>
        /// Gets the distinguished folder corresponding to the specified ID and e-mail address.
        /// </summary>
        /// <param name="id">ID of the requested folder</param>
        /// <param name="emailAddress">E-mail address of the requested folder</param>
        /// <returns>
        /// The distinguished folder.
        /// </returns>
        internal static DistinguishedFolderIdType GetDistinguishedFolder(DistinguishedFolderIdNameType id, string emailAddress)
        {
            Debug.Assert(!String.IsNullOrEmpty(emailAddress), "!String.IsNullOrEmpty(emailAddress)");

            EmailAddressType mailbox = new EmailAddressType();
            mailbox.EmailAddress = emailAddress;

            DistinguishedFolderIdType folder = new DistinguishedFolderIdType();
            folder.Id = id;
            folder.Mailbox = mailbox;

            return folder;
        }

        /// <summary>
        /// Creates a notification item for the specified calendar item.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="emailAddress">E-mail address for the recipient of this notification</param>
        /// <param name="displayName">Display name for the recipient of this notification</param>
        /// <param name="calendarItem">Mailbox item</param>
        /// <returns>
        /// A notification item, or null if the mailbox item does not meet the criteria for an enhanced reminder.
        /// </returns>
        internal static NotificationItem[] CreateCalendarNotificationItem(Context context, string emailAddress, string displayName, CalendarItemType calendarItem)
        {
            Debug.Assert(context != null, "context != null");
            Debug.Assert(!String.IsNullOrEmpty(emailAddress), "!String.IsNullOrEmpty(emailAddress)");
            Debug.Assert(!String.IsNullOrEmpty(displayName), "!String.IsNullOrEmpty(displayName)");
            Debug.Assert(calendarItem != null, "calendarItem != null");
            Debug.Assert(calendarItem.ItemId != null, "Calendar item ID should not be null");

            // Get the extended property that contains the reminder details
            string reminderExtendedPropertyValue = GetReminderFromExtendedProperties(calendarItem);

            if (String.IsNullOrEmpty(reminderExtendedPropertyValue))
            {
                // The reminder property is not present
                Logger.WriteTraceWarning(context, "The reminder property is not present.");
                return null;
            }

            ReminderItem reminderItem = EnhancedRemindersHelpers.DeserializeReminderItem(context, reminderExtendedPropertyValue);

            if (reminderItem == null)
            {
                // Failed to extract the reminder from the reminder extended property
                Logger.WriteTraceWarning(context, "Failed to extract the reminder from the reminder extended property.");
                return null;
            }

            return CreateCalendarNotificationItemsFromReminderItem(context, emailAddress, displayName, calendarItem, reminderItem, new NotificationId(calendarItem.ItemId.Id, emailAddress, calendarItem.ItemId.ChangeKey));
        }

        /// <summary>
        /// Creates an array of notification items for the specified calendar item and reminder item. Both parameters
        /// are required in the case of recurring meetings, where the occurrence calendar item does not have a reminder
        /// item directly associated with it and we need to instead include the reminder item and appropriate notification
        /// ID from the recurring master.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="emailAddress">E-mail address for the recipient of this notification</param>
        /// <param name="displayName">Display name for the recipient of this notification</param>
        /// <param name="calendarItem">Mailbox item</param>
        /// <param name="reminderItem">The reminder item associated with the calendar item</param>
        /// <param name="notificationId">The notification ID for the requested NotificationItems.</param>
        /// <returns>
        /// A notification item, or null if the mailbox item does not meet the criteria for an enhanced reminder.
        /// </returns>
        internal static NotificationItem[] CreateCalendarNotificationItemsFromReminderItem(Context context, string emailAddress, string displayName, CalendarItemType calendarItem, ReminderItem reminderItem, NotificationId notificationId)
        {
            Debug.Assert(context != null, "context != null");
            Debug.Assert(!String.IsNullOrEmpty(emailAddress), "!String.IsNullOrEmpty(emailAddress)");
            Debug.Assert(!String.IsNullOrEmpty(displayName), "!String.IsNullOrEmpty(displayName)");
            Debug.Assert(calendarItem != null, "calendarItem != null");
            Debug.Assert(calendarItem.ItemId != null, "Calendar item ID should not be null");
            Debug.Assert(reminderItem != null, "reminderItem != null");
            Debug.Assert(notificationId != null, "notificationId != null");

            // Get the extended property that contains when the reminder was delivered.
            long deliveryTime;
            bool hasDeliveryTime = long.TryParse(GetDeliveredTimeFromExtendedProperties(calendarItem), out deliveryTime);

            if (reminderItem.IsJoinNowEnabled &&
                reminderItem.ConferenceType == ConferenceType.None &&
                GetJoinNowRuleIdFromExtendedProperties(calendarItem) != null)
            {
                // This is a rules-based JoinNow so need to extract the Live Meeting details from the calendar item
                string conferenceAddress = ExtractConferenceAddress(context, calendarItem);

                if (!String.IsNullOrEmpty(conferenceAddress))
                {
                    Logger.WriteTraceInformation(context, "Adding Live Meeting conference address to reminder: {0}", conferenceAddress);
                    reminderItem.ConferenceType = ConferenceType.LiveMeeting;
                    reminderItem.ConferenceAddress = conferenceAddress;
                }
                else
                {
                    Logger.WriteTraceWarning(context, "JoinInfo property could not be read from Live Meeting request.");
                }
            }

            List<NotificationItem> notificationList = new List<NotificationItem>();

            if (reminderItem.IsNotificationEnabled)
            {
                DateTime reminderDueTime = reminderItem.GetDueTime(calendarItem.Start, false);

                if (!hasDeliveryTime || reminderDueTime.Ticks > deliveryTime)
                {
                    // Create the notification item
                    if (calendarItem.IsMeeting)
                    {
                        notificationList.Add(new MeetingNotificationItem(
                            context,
                            notificationId,
                            emailAddress,
                            reminderDueTime,
                            reminderItem,
                            displayName,
                            false /* hasBody */,
                            calendarItem.Location,
                            calendarItem.Subject,
                            calendarItem.Start,
                            calendarItem.Organizer.Item.Name,
                            false));
                    }
                    else
                    {
                        notificationList.Add(new CalendarNotificationItem(
                            context,
                            notificationId,
                            emailAddress,
                            reminderDueTime,
                            reminderItem,
                            displayName,
                            false /* hasBody */,
                            calendarItem.Location,
                            calendarItem.Subject,
                            calendarItem.Start));
                    }
                }
                else
                {
                    Logger.WriteTraceVerbose(context, "Notification was filtered because it has already been delivered.");
                }
            }

            if (reminderItem.IsJoinNowEnabled && reminderItem.ConferenceType != ConferenceType.None)
            {
                // While standard notifications and Join Now may be on the same calendar item, it's possible
                // that one item has already been delivered while the other should still be scheduled. (e.g.,
                // in the case that a reminder is scheduled on the night before a meeting and a Join Now notification
                // is scheduled at the meeting start time.
                DateTime reminderDueTime = reminderItem.GetDueTime(calendarItem.Start, true);

                if (!hasDeliveryTime || reminderDueTime.Ticks > deliveryTime)
                {
                    notificationList.Add(new MeetingNotificationItem(
                        context,
                        notificationId,
                        emailAddress,
                        reminderDueTime,
                        reminderItem,
                        displayName,
                        false /* hasBody */,
                        calendarItem.Location,
                        calendarItem.Subject,
                        calendarItem.Start,
                        calendarItem.Organizer.Item.Name,
                        true));
                }
                else
                {
                    Logger.WriteTraceVerbose(context, "Join Now notification was filtered because it has already been delivered.");
                }
            }

            return notificationList.Count > 0 ? notificationList.ToArray() : null;
        }

        /// <summary>
        /// Extracts the conference address from the JoinInfo property on the specified calendar item.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="calendarItem">Calendar item</param>
        /// <returns>
        /// The conference address or null if the address cannot be extracted.
        /// </returns>
        private static string ExtractConferenceAddress(Context context, CalendarItemType calendarItem)
        {
            string encodedJoinInfoProperty = GetJoinInfoFromExtendedProperties(calendarItem);

            if (String.IsNullOrEmpty(encodedJoinInfoProperty))
            {
                // The JoinInfo property is not specified
                return null;
            }

            // Determine how much padding is needed to ensure that the base 64 encoded string is a multiple of 4
            int padLength = encodedJoinInfoProperty.Length % 4 > 0 ? 4 - (encodedJoinInfoProperty.Length % 4) : 0;

            if (padLength > 2)
            {
                // Should not need to pad more than two characters
                return null;
            }

            // Ensure the base 64 encoded string is a multiple of 4
            encodedJoinInfoProperty = encodedJoinInfoProperty.PadRight(encodedJoinInfoProperty.Length + padLength, '=');

            byte[] decodedJoinInfoProperty = null;

            try
            {
                // Decode the base 64 endcoded property value
                decodedJoinInfoProperty = Convert.FromBase64String(encodedJoinInfoProperty);
            }
            catch (FormatException ex)
            {
                // The specified string has an invalid format
                Logger.WriteTraceWarning(context, ex, "Failed to decode the base 64 encoded JoinInfo property.");
            }

            if (decodedJoinInfoProperty == null)
            {
                return null;
            }

            MemoryStream stream = new MemoryStream(decodedJoinInfoProperty);

            // Read the unicode character stream
            StreamReader reader = new StreamReader(stream, System.Text.Encoding.Unicode);
            string joinInfo = reader.ReadToEnd();
            Debug.Assert(!String.IsNullOrEmpty(joinInfo), "!String.IsNullOrEmpty(joinInfo)");

            // Find the start of the XML tag containing the conference address
            int startIndex = joinInfo.IndexOf(_confUriStart, 0, StringComparison.OrdinalIgnoreCase);

            if (startIndex == -1)
            {
                return null;
            }

            startIndex += _confUriStart.Length;

            // Find the end of the XML tag containing the conference address
            int endIndex = joinInfo.IndexOf(_confUriEnd, startIndex, StringComparison.OrdinalIgnoreCase);

            if (endIndex == -1)
            {
                return null;
            }

            // Extract the conference address from the XML
            return joinInfo.Substring(startIndex, endIndex - startIndex);
        }

        /// <summary>
        /// Gets the rules that match the specified mailbox item.
        /// </summary>
        /// <param name="rules">Rules to match against</param>
        /// <param name="item">Mailbox item to compare to the rules</param>
        /// <returns>
        /// The rules that match the specified mailbox item.
        /// </returns>
        internal static NotificationRuleItem[] GetMatchingRules(NotificationRules rules, ItemType item)
        {
            Debug.Assert(rules != null, "rules != null");
            Debug.Assert(item != null, "item != null");

            NotificationData notificationData = new NotificationData(
                GetNameFromExtendedProperties(item),    // Sender
                GetMailboxTypeFromMessage(item),        // Type of mailbox item
                item.DateTimeReceived);                 // Time mailbox item was received

            return rules.GetMatchingRules(notificationData);
        }

        /// <summary>
        /// Creates a notification item for the specified voicemail message item using the specified rules.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="emailAddress">E-mail address for the recipient of this notification</param>
        /// <param name="displayName">Display name for the recipient of this notification</param>
        /// <param name="rules">Rules to use in constructing the notification</param>
        /// <param name="item">Voicemail message</param>
        /// <param name="fileAttachment">Voicemail audio attachment</param>
        /// <returns>
        /// The notification item for the specified voicemail.
        /// </returns>
        internal static NotificationItem CreateVoicemailNotificationItem(Context context, string emailAddress, string displayName, NotificationRules rules, ItemType item, FileAttachmentType fileAttachment)
        {
            Debug.Assert(context != null, "context != null");
            Debug.Assert(!String.IsNullOrEmpty(emailAddress), "!String.IsNullOrEmpty(emailAddress)");
            Debug.Assert(!String.IsNullOrEmpty(displayName), "!String.IsNullOrEmpty(displayName)");
            Debug.Assert(rules != null, "rules != null");
            Debug.Assert(item != null, "item != null");
            Debug.Assert(GetMailboxTypeFromMessage(item) == MailboxType.Voicemail, "GetMailboxTypeFromMessage(item) == MailboxType.Voicemail");
            Debug.Assert(fileAttachment != null && fileAttachment.Content != null, "fileAttachment != null && fileAttachment.Content != null");

            // Find matching rule(s)
            NotificationRuleItem[] matchingRules = GetMatchingRules(rules, item);

            // There should always be a matching rule as otherwise processing shouldn't have reached this far
            Debug.Assert(matchingRules != null, "matchingRules != null");
            Debug.Assert(matchingRules.Length > 0, "matchingRules.Length > 0");

            // Get the extended property that contains when the reminder was delivered.
            long deliveryTime;
            bool hasDeliveryTime = long.TryParse(GetDeliveredTimeFromExtendedProperties(item), out deliveryTime);

            if (!hasDeliveryTime || item.DateTimeReceived.Ticks > deliveryTime)
            {
                // Create the notification item
                return new VoicemailNotificationItem(
                    context,
                    new NotificationId(item.ItemId.Id, emailAddress, item.ItemId.ChangeKey),
                    emailAddress,
                    item.DateTimeReceived,
                    NotificationRuleItem.CreateReminderItems(matchingRules),
                    displayName,
                    fileAttachment.Content,
                    fileAttachment.Name.EndsWith("wma", StringComparison.OrdinalIgnoreCase),
                    GetCallerIdFromExtendedProperties(item));
            }
            else
            {
                Logger.WriteTraceVerbose(context, "Voicemail reminder not scheduled because it has already been delivered");
                return null;
            }
        }

        /// <summary>
        /// Gets the path to the specified extended property.
        /// </summary>
        /// <returns>
        /// The path to the extended property.
        /// </returns>
        private static PathToExtendedFieldType GetExtendedPropertyPath(string setId, string name)
        {
            Debug.Assert(!String.IsNullOrEmpty(setId), "!String.IsNullOrEmpty(setId)");
            Debug.Assert(!String.IsNullOrEmpty(name), "!String.IsNullOrEmpty(name)");

            // Path to extended property that stores the specified rule ID
            PathToExtendedFieldType path = new PathToExtendedFieldType();
            path.PropertySetId = setId;
            path.PropertyName = name;
            path.PropertyType = MapiPropertyTypeType.String;
            return path;
        }

        /// <summary>
        /// Gets the path to the extended property containing the rule ID for rules-based reminders.
        /// </summary>
        /// <returns>
        /// The path to the extended property.
        /// </returns>
        internal static PathToExtendedFieldType GetExtendedPropertyPathForReminderRuleId()
        {
            return GetExtendedPropertyPath(Constants.ReminderRuleIdPropertyId, Constants.ReminderRuleIdPropertyName);
        }

        /// <summary>
        /// Gets the path to the extended property containing the rule ID for rules-based JoinNow.
        /// </summary>
        /// <returns>
        /// The path to the extended property.
        /// </returns>
        internal static PathToExtendedFieldType GetExtendedPropertyPathForJoinNowRuleId()
        {
            return GetExtendedPropertyPath(Constants.JoinNowRuleIdPropertyId, Constants.JoinNowRuleIdPropertyName);
        }

        /// <summary>
        /// Gets the path to the extended property containing the enhamced reminder information.
        /// </summary>
        /// <returns>
        /// The path to the extended property.
        /// </returns>
        internal static PathToExtendedFieldType GetExtendedPropertyPathForReminder()
        {
            return GetExtendedPropertyPath(Constants.ReminderPropertyId, Constants.ReminderPropertyName);
        }

        /// <summary>
        /// Gets the path to the extended property containing the join information for Live Meetings.
        /// </summary>
        /// <returns>
        /// The path to the extended property.
        /// </returns>
        internal static PathToExtendedFieldType GetExtendedPropertyPathForJoinInfo()
        {
            return GetExtendedPropertyPath(Constants.JoinInfoPropertyId, Constants.JoinInfoPropertyName);
        }

        /// <summary>
        /// Gets the path to the extended property containing when a notification was delivered.
        /// </summary>
        /// <returns>The path to the extended property.</returns>
        internal static PathToExtendedFieldType GetExtendedPropertyPathForReminderDelivered()
        {
            return GetExtendedPropertyPath(Constants.ReminderDeliveredPropertyId, Constants.ReminderDeliveredPropertyName);
        }

        /// <summary>
        /// Gets the path to the extended property containing flag for when a notification on a recurring meeting exception was deleted.
        /// </summary>
        /// <returns>The path to the extended property.</returns>
        internal static PathToExtendedFieldType GetExtendedPropertyPathForExceptionDeletedFromExtendedProperties()
        {
            return GetExtendedPropertyPath(Constants.ReminderOnExceptionDeletedPropertyId, Constants.ReminderOnExceptionDeletedPropertyName);
        }

        /// <summary>
        /// Gets the search expression for enhanced reminders.
        /// </summary>
        /// <remarks>
        /// Enhanced reminders are identified by the presence of a particular extended property.
        /// </remarks>
        /// <returns>
        /// The search expression.
        /// </returns>
        internal static SearchExpressionType GetRemindersExpression()
        {
            ExistsType reminderPropertyExists = new ExistsType();
            reminderPropertyExists.Item = GetExtendedPropertyPathForReminder();
            return reminderPropertyExists;
        }

        /// <summary>
        /// Gets the search expression from the specified rules for the specified mailbox type.
        /// </summary>
        /// <param name="context">Logging context</param>
        /// <param name="rules">Rules</param>
        /// <param name="mailboxType">Mailbox type</param>
        /// <remarks>
        /// This method gets the search expression for each individual rule and combines them using an "or" expression.
        /// </remarks>
        /// <returns>
        /// The search expression, or null if there are no rules of the specified type.
        /// </returns>
        internal static SearchExpressionType GetSearchExpressionFromRules(Context context, NotificationRules rules, MailboxType mailboxType)
        {
            SearchExpressionType rulesSearchExpression = null;

            if (rules != null && rules.Count > 0)
            {
                List<SearchExpressionType> searchExpressionList = new List<SearchExpressionType>();

                foreach (NotificationRuleItem rule in rules)
                {
                    if (rule.IsEnabled && rule.MailboxType == mailboxType)
                    {
                        SearchExpressionType ruleSearchExpression = null;

                        if (rule.MailboxType == MailboxType.Voicemail)
                        {
                            ruleSearchExpression = GetSearchExpressionFromVoicemailRule(rule);
                        }
                        else if (rule.MailboxType == MailboxType.CalendarItem)
                        {
                            ruleSearchExpression = GetSearchExpressionFromCalendarRule(rule);
                        }
                        else
                        {
                            Logger.WriteTraceWarning(context, "Unsupported mailbox type: {0} on rule: {1}", rule.MailboxType, rule.RuleName);
                        }

                        if (ruleSearchExpression != null)
                        {
                            searchExpressionList.Add(ruleSearchExpression);
                        }

                        // NOTE: Currently there is no enforced limit on the number of rules. However,
                        // in the interest of preventing certain potential security issues an enterprise
                        // targeted application should enforce a limit on the number of rules a user can define.
                    }
                }

                if (searchExpressionList.Count == 1)
                {
                    rulesSearchExpression = searchExpressionList[0];
                }
                else if (searchExpressionList.Count > 1)
                {
                    // More than one search expression, so need to or them together
                    OrType orSearchExpression = new OrType();
                    orSearchExpression.Items = searchExpressionList.ToArray();
                    rulesSearchExpression = orSearchExpression;
                }
            }

            return rulesSearchExpression;
        }

        /// <summary>
        /// Gets the search expression for the specified rule.
        /// </summary>
        /// <param name="rule">Rule</param>
        /// <remarks>
        /// This method gets the search expression for each component of the rule and combines them using an "and" expression.
        /// </remarks>
        /// <returns>
        /// The search expression, or null if the rule is invalid.
        /// </returns>
        private static SearchExpressionType GetSearchExpressionFromCalendarRule(NotificationRuleItem rule)
        {
            Debug.Assert(rule != null, "rule != null");
            Debug.Assert(rule.IsEnabled, "rule.IsEnabled");

            // Ignore calendar items that already have an enhanced reminder
            NotType notRemindersSearchExpression = new NotType();
            notRemindersSearchExpression.Item = GetRemindersExpression();

            // Ignore calendar items whose start time is in the past (for the sake of efficiency)
            IsGreaterThanOrEqualToType timeSearchExpression = GetSearchExpressionForCalendarStart(DateTime.UtcNow - SchedulerService._sendCutoffTimePeriod);

            AndType ruleSearchExpression = new AndType();
            ruleSearchExpression.Items = new SearchExpressionType[] { notRemindersSearchExpression, timeSearchExpression };

            return ruleSearchExpression;
        }

        /// <summary>
        /// Gets the search expression for the specified rule.
        /// </summary>
        /// <param name="rule">Rule</param>
        /// <remarks>
        /// This method gets the search expression for each component of the rule and combines them using an "and" expression.
        /// </remarks>
        /// <returns>
        /// The search expression, or null if the rule is invalid.
        /// </returns>
        private static SearchExpressionType GetSearchExpressionFromVoicemailRule(NotificationRuleItem rule)
        {
            Debug.Assert(rule != null, "rule != null");
            Debug.Assert(rule.IsEnabled, "rule.IsEnabled");

            // Voicemail are identified by the item class
            PathToUnindexedFieldType itemItemClassPath = new PathToUnindexedFieldType();
            itemItemClassPath.FieldURI = UnindexedFieldURIType.itemItemClass;
            ContainsExpressionType voicemailSearchExpression = GetContainsExpression(_voicemailItemClass, itemItemClassPath, ContainmentModeType.ExactPhrase);

            // Ensure that the search expression only includes items received after the rule was created
            SearchExpressionType timeSearchExpression = GetSearchExpressionForTimeReceived(rule.Timestamp);
            Debug.Assert(timeSearchExpression != null, "timeSearchExpression != null");

            // Restrict the search expression to the senders associated with this rule
            SearchExpressionType senderSearchExpression = GetSenderSearchExpressionFromRule(rule);

            AndType ruleSearchExpression = new AndType();

            if (senderSearchExpression != null)
            {
                ruleSearchExpression.Items = new SearchExpressionType[] { voicemailSearchExpression, senderSearchExpression, timeSearchExpression };
            }
            else
            {
                // No sender conditions associated with this rule
                ruleSearchExpression.Items = new SearchExpressionType[] { voicemailSearchExpression, timeSearchExpression };
            }

            return ruleSearchExpression;
        }

        /// <summary>
        /// Gets the search expression for the senders associated with the specified rule.
        /// </summary>
        /// <param name="rule">Rule</param>
        /// <remarks>
        /// </remarks>
        /// This method creates a search expression for each sender and combines them using an "or" expression.
        /// <returns>
        /// The search expression, or null if no senders are associated with this rule.
        /// </returns>
        private static SearchExpressionType GetSenderSearchExpressionFromRule(NotificationRuleItem rule)
        {
            Debug.Assert(rule != null, "rule != null");
            Debug.Assert(rule.IsEnabled, "rule.IsEnabled");
            Debug.Assert(rule.Conditions != null, "rule.Conditions != null");

            SearchExpressionType senderExpression = null;

            if (rule.Conditions != null)
            {
                List<SearchExpressionType> searchExpressionList = new List<SearchExpressionType>();

                NotificationPeopleCondition peopleCondition = Helpers.AssertCast<NotificationPeopleCondition>(rule.GetCondition(typeof(NotificationPeopleCondition)));

                if (peopleCondition != null && peopleCondition.IsSelected)
                {
                    Collection<string> names = peopleCondition.Names;

                    if (names != null && names.Count > 0)
                    {
                        // The value of the PR_SENT_REPRESENTING_NAME field should match one of the names specified in the rule
                        PathToExtendedFieldType fromPath = GetExtendedStringProperty(FromNamePropertyTag);

                        foreach (string name in names)
                        {
                            // NOTE: Currently there is no enforced limit on the number of senders. However,
                            // in the interest of preventing certain potential security issues an enterprise
                            // targeted application should enforce a limit on the number of senders a user can define.

                            ConstantValueType constantName = new ConstantValueType();
                            constantName.Value = name;

                            IsEqualToType equalsName = new IsEqualToType();
                            equalsName.FieldURIOrConstant = new FieldURIOrConstantType();
                            equalsName.FieldURIOrConstant.Item = constantName;
                            equalsName.Item = fromPath;

                            searchExpressionList.Add(equalsName);
                        }
                    }
                }

                if (searchExpressionList.Count == 1)
                {
                    senderExpression = searchExpressionList[0];
                }
                else if (searchExpressionList.Count > 1)
                {
                    // More than one search expression, so need to or them together
                    OrType orSenderExpression = new OrType();
                    orSenderExpression.Items = searchExpressionList.ToArray();
                    senderExpression = orSenderExpression;
                }
            }

            return senderExpression;
        }

        /// <summary>
        /// Gets the search expression for storage items.
        /// </summary>
        /// <remarks>
        /// Storage items are identified by the subject, sender, from and attachment name fields. Sender and from fields are 
        /// included to prevent a valid storage item being sent to an unsuspecting user.
        /// </remarks>
        /// <returns>
        /// The search expression.
        /// </returns>
        internal static SearchExpressionType GetStorageItemExpression()
        {
            // Storage items have a specific subject
            PathToUnindexedFieldType subjectPath = new PathToUnindexedFieldType();
            subjectPath.FieldURI = UnindexedFieldURIType.itemSubject;
            ContainsExpressionType containsSubject = GetContainsExpression(Constants.StorageItemName, subjectPath, ContainmentModeType.ExactPhrase);

            // Storage items have a blank sender field (this prevents storage items being sent)
            PathToUnindexedFieldType senderPath = new PathToUnindexedFieldType();
            senderPath.FieldURI = UnindexedFieldURIType.messageSender;
            ExistsType existsSender = new ExistsType();
            existsSender.Item = senderPath;
            NotType notExistsSender = new NotType();
            notExistsSender.Item = existsSender;

            // Storage items have a blank from field (this prevents storage items being sent)
            PathToUnindexedFieldType fromPath = new PathToUnindexedFieldType();
            fromPath.FieldURI = UnindexedFieldURIType.messageFrom;
            ExistsType existsFrom = new ExistsType();
            existsFrom.Item = fromPath;
            NotType notExistsFrom = new NotType();
            notExistsFrom.Item = existsFrom;

            // Storage items have a specific named attachment
            ContainsExpressionType containsAttachment = GetAttachmentExpression();

            AndType storageItemExpression = new AndType();
            storageItemExpression.Items = new SearchExpressionType[] { containsSubject, notExistsSender, notExistsFrom, containsAttachment };

            return storageItemExpression;
        }

        /// <summary>
        /// Gets the search expression for enhanced reminders that occur in the future as well as the master item
        /// for any recurring meetings.
        /// </summary>
        /// <remarks>
        /// Enhanced reminders are identified by the presence of a particular extended property. Furthermore, only reminders 
        /// in the future are of interest.
        /// 
        /// All recurring items must be fetched as exceptions may have an enhanced reminder on them and cannot be
        /// included in the search expression.
        /// </remarks>
        /// <returns>
        /// The search expression.
        /// </returns>
        internal static SearchExpressionType GetFutureAndRecurringRemindersExpression()
        {
            PathToExtendedFieldType recurrencePath = new PathToExtendedFieldType();
            recurrencePath.PropertySetId = new Guid("{00062002-0000-0000-C000-000000000046}").ToString("D");
            recurrencePath.PropertyId = 0x8216;
            recurrencePath.PropertyIdSpecified = true;
            recurrencePath.PropertyType = MapiPropertyTypeType.Binary;
            
            ExistsType existsType = new ExistsType();
            existsType.Item = recurrencePath;
            
            OrType remindersOrRecurrence = new OrType();

            AndType futureRemindersExpression = new AndType();
            futureRemindersExpression.Items = new SearchExpressionType[] { GetRemindersExpression(),
                GetSearchExpressionForCalendarStart(DateTime.UtcNow - SchedulerService._sendCutoffTimePeriod) };

            remindersOrRecurrence.Items = new SearchExpressionType[] { existsType, futureRemindersExpression};

            return remindersOrRecurrence;
        }

        /// <summary>
        /// Gets a search expression for calendar items that start after the specified time.
        /// </summary>
        /// <param name="time">Time</param>
        /// <returns>
        /// The search expression.
        /// </returns>
        internal static IsGreaterThanOrEqualToType GetSearchExpressionForCalendarStart(DateTime time)
        {
            PathToUnindexedFieldType calendarStartField = new PathToUnindexedFieldType();
            calendarStartField.FieldURI = UnindexedFieldURIType.calendarStart;
            return GetSearchExpressionFromDateTime(time, calendarStartField);
        }

        /// <summary>
        /// Gets a search expression for mailbox items received after the specified time.
        /// </summary>
        /// <param name="time">Time</param>
        /// <returns>
        /// The search expression.
        /// </returns>
        internal static IsGreaterThanOrEqualToType GetSearchExpressionForTimeReceived(DateTime time)
        {
            PathToUnindexedFieldType timeReceivedField = new PathToUnindexedFieldType();
            timeReceivedField.FieldURI = UnindexedFieldURIType.itemDateTimeReceived;
            return GetSearchExpressionFromDateTime(time, timeReceivedField);
        }

        /// <summary>
        /// Gets a search expression for mailbox items which have a specified time field that is after the specified time.
        /// </summary>
        /// <param name="time">Time value</param>
        /// <param name="timePath">Path to the time field to be searched</param>
        /// <returns>
        /// The search expression.
        /// </returns>
        private static IsGreaterThanOrEqualToType GetSearchExpressionFromDateTime(DateTime time, PathToUnindexedFieldType timePath)
        {
            ConstantValueType timeConstantValue = new ConstantValueType();
            timeConstantValue.Value = time.ToString(CultureInfo.InvariantCulture.DateTimeFormat);

            FieldURIOrConstantType timeConstantItem = new FieldURIOrConstantType();
            timeConstantItem.Item = timeConstantValue;

            IsGreaterThanOrEqualToType timeSearchExpression = new IsGreaterThanOrEqualToType();
            timeSearchExpression.FieldURIOrConstant = timeConstantItem;
            timeSearchExpression.Item = timePath;

            return timeSearchExpression;
        }

        /// <summary>
        /// Gets the search expression for enhanced reminders and storage item attachments.
        /// </summary>
        /// <returns>
        /// The search expression.
        /// </returns>
        internal static ContainsExpressionType GetAttachmentExpression()
        {
            PathToUnindexedFieldType attachmentPath = new PathToUnindexedFieldType();
            attachmentPath.FieldURI = UnindexedFieldURIType.itemAttachments;
            return GetContainsExpression(Constants.AttachmentName, attachmentPath, ContainmentModeType.Substring);
        }

        /// <summary>
        /// Gets the search expression for the specified value in the specified field.
        /// </summary>
        /// <param name="value">Value to be searched for</param>
        /// <param name="pathToField">Path to the field in which to search for the specified value</param>
        /// <param name="mode">Type of comparison to perform</param>
        /// <returns>
        /// The search expression.
        /// </returns>
        internal static ContainsExpressionType GetContainsExpression(string value, BasePathToElementType pathToField, ContainmentModeType mode)
        {
            ContainsExpressionType containsSearchExpression = new ContainsExpressionType();
            containsSearchExpression.ContainmentComparison = ContainmentComparisonType.Exact;
            containsSearchExpression.ContainmentComparisonSpecified = true;
            containsSearchExpression.ContainmentMode = mode;
            containsSearchExpression.ContainmentModeSpecified = true;
            containsSearchExpression.Constant = new ConstantValueType();
            containsSearchExpression.Constant.Value = value;
            containsSearchExpression.Item = pathToField;
            return containsSearchExpression;
        }

        /// <summary>
        /// Gets the path to the specified extended string property.
        /// </summary>
        /// <returns>
        /// The path to the extended property.
        /// </returns>
        internal static PathToExtendedFieldType GetExtendedStringProperty(string propertyTag)
        {
            PathToExtendedFieldType path = new PathToExtendedFieldType();
            path.PropertyTag = propertyTag;
            path.PropertyType = MapiPropertyTypeType.String;
            return path;
        }

        /// <summary>
        /// Gets the reminder from the appropriate extended property of the specified mailbox item.
        /// </summary>
        /// <param name="item">Mailbox item containing the rule ID</param>
        /// <returns>
        /// The reminder or null if no reminder is found
        /// </returns>
        internal static string GetReminderFromExtendedProperties(ItemType item)
        {
            Debug.Assert(item != null, "item != null");
            return GetExtendedPropertyByPropertyName(item, Constants.ReminderPropertyName);
        }

        /// <summary>
        /// Get the flag indicating whether the reminder on an exception to a recurring meeting was explicitly deleted.
        /// </summary>
        /// <param name="item">Mailbox item containing the flag.</param>
        /// <returns>
        /// True if the property is present and set to true; false otherwise.
        /// </returns>
        internal static bool GetReminderOnExceptionDeletedFromExtendedProperties(ItemType item)
        {
            Debug.Assert(item != null, "item != null");

            string property = GetExtendedPropertyByPropertyName(item, Constants.ReminderOnExceptionDeletedPropertyName);

            Debug.Assert(property == null ||
                         property.Equals("true", StringComparison.CurrentCultureIgnoreCase) ||
                         property.Equals("false", StringComparison.CurrentCultureIgnoreCase),
                         "Unexpected value for ReminderOnExceptionDeleted property");
            
            return property == null ? false : bool.Parse(property);
        }

        /// <summary>
        /// Gets the reminder rule ID from the appropriate extended property of the specified mailbox item.
        /// </summary>
        /// <param name="item">Mailbox item containing the rule ID</param>
        /// <returns>
        /// The rule ID or null if no rule ID is found
        /// </returns>
        internal static string GetReminderRuleIdFromExtendedProperties(ItemType item)
        {
            Debug.Assert(item != null, "item != null");
            return GetExtendedPropertyByPropertyName(item, Constants.ReminderRuleIdPropertyName);
        }

        /// <summary>
        /// Gets the JoinNow rule ID from the appropriate extended property of the specified mailbox item.
        /// </summary>
        /// <param name="item">Mailbox item containing the rule ID</param>
        /// <returns>
        /// The rule ID or null if no rule ID is found
        /// </returns>
        internal static string GetJoinNowRuleIdFromExtendedProperties(ItemType item)
        {
            Debug.Assert(item != null, "item != null");
            return GetExtendedPropertyByPropertyName(item, Constants.JoinNowRuleIdPropertyName);
        }

        /// <summary>
        /// Gets the time a reminder was delivered from the appropriate extended property of the specified mailbox
        /// item.
        /// </summary>
        /// <param name="item">Mailbox item that had a reminder delivered</param>
        /// <returns>The ticks representing DateTime (in UTC) that the reminder was delivered, or null if the reminder was never delivered</returns>
        internal static string GetDeliveredTimeFromExtendedProperties(ItemType item)
        {
            Debug.Assert(item != null, "item != null");
            return GetExtendedPropertyByPropertyName(item, Constants.ReminderDeliveredPropertyName);
        }

        /// <summary>
        /// Gets the caller ID from the appropriate extended property of the specified mailbox item.
        /// </summary>
        /// <param name="item">Mailbox item containing the caller ID</param>
        /// <returns>
        /// The caller ID or null if no caller ID is found
        /// </returns>
        private static string GetCallerIdFromExtendedProperties(ItemType item)
        {
            Debug.Assert(item != null, "item != null");
            return GetExtendedPropertyByPropertyTag(item, CallerIdPropertyTag);
        }

        /// <summary>
        /// Gets the name of who the message is from, from the appropriate extended property of the specified mailbox item.
        /// </summary>
        /// <param name="item">Mailbox item containing the name</param>
        /// <returns>
        /// The name or null if no name is found
        /// </returns>
        private static string GetNameFromExtendedProperties(ItemType item)
        {
            Debug.Assert(item != null, "item != null");
            return GetExtendedPropertyByPropertyTag(item, FromNamePropertyTag);
        }

        /// <summary>
        /// Gets the join info from the appropriate extended property of the specified mailbox item.
        /// </summary>
        /// <param name="item">Mailbox item containing the join info</param>
        /// <returns>
        /// The join info or null if the property is not found.
        /// </returns>
        private static string GetJoinInfoFromExtendedProperties(ItemType item)
        {
            Debug.Assert(item != null, "item != null");
            return GetExtendedPropertyByPropertyName(item, Constants.JoinInfoPropertyName);
        }

        /// <summary>
        /// Gets the value of the specified extended property from the specified mailbox item.
        /// </summary>
        /// <param name="item">The mailbox item</param>
        /// <param name="propertyTag">The property tag of the requested extended property</param>
        /// <returns>
        /// The value of the extended property or null if the property is not specified.
        /// </returns>
        private static string GetExtendedPropertyByPropertyTag(ItemType item, string propertyTag)
        {
            Debug.Assert(item != null, "item != null");
            Debug.Assert(!String.IsNullOrEmpty(propertyTag), "!String.IsNullOrEmpty(propertyTag)");

            if (item.ExtendedProperty != null)
            {
                for (int i = 0; i < item.ExtendedProperty.Length; i++)
                {
                    if (item.ExtendedProperty[i].ExtendedFieldURI.PropertyTag == propertyTag)
                    {
                        return item.ExtendedProperty[i].Item as string;
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// Gets the value of the specified extended property from the specified mailbox item.
        /// </summary>
        /// <param name="item">The mailbox item</param>
        /// <param name="propertyName">The property name of the requested extended property</param>
        /// <returns>
        /// The value of the extended property or null if the property is not specified.
        /// </returns>
        private static string GetExtendedPropertyByPropertyName(ItemType item, string propertyName)
        {
            Debug.Assert(item != null, "item != null");
            Debug.Assert(!String.IsNullOrEmpty(propertyName), "!String.IsNullOrEmpty(propertyName)");

            if (item.ExtendedProperty != null)
            {
                for (int i = 0; i < item.ExtendedProperty.Length; i++)
                {
                    if (item.ExtendedProperty[i].ExtendedFieldURI.PropertyName == propertyName)
                    {
                        return item.ExtendedProperty[i].Item as string;
                    }
                }
            }

            return null;
        }
    }
}
